Skip to content
google-sheets logo

Google Sheets Automation

google-sheets

Google Sheets automation using Python gspread library - reading, writing, formatting, and Service Account setup

SKILL.md

Full skill instructions

Google Sheets Automation

Focus: Automating Google Sheets with Python using gspread library

Source: Patterns from Meta Ads → Google Sheets automation project (ss-automation)


When to Use This Skill

Use google-sheets when:

  • ✓ Automating data export to Google Sheets
  • ✓ Building dashboards that update from external data
  • ✓ Creating scheduled reports in Sheets format
  • ✓ Processing data from APIs and writing to Sheets
  • ✓ Setting up Service Account authentication

DO NOT use for:

  • ✗ Complex spreadsheet logic (use Google Apps Script for that)
  • ✗ Real-time collaborative editing (Sheets UI is better)
  • ✗ Simple one-time manual edits

Why Python + gspread (Not Apps Script)

Criteriagspread + PythonGoogle Apps Script
External API integration✅ Native (requests, SDKs)⚠️ UrlFetchApp (limited)
Data manipulation✅ pandas, numpy❌ JavaScript arrays only
Version control✅ git, code review❌ Web editor only
Testing✅ pytest, mocks❌ Manual testing
Scheduling✅ cron, Lambda, GitHub Actions✅ Built-in triggers
Execution time✅ Unlimited❌ 6-minute limit
Cost$0-5/​month$0

Decision: Use gspread + Python when integrating external data (APIs, databases). Use Apps Script for simple sheet-internal automation.


Quick Start

1. Install Dependencies

pip install gspread google-auth gspread-formatting

2. Create Service Account (One-Time Setup)

See SETUP.md for detailed instructions.

Quick version:

  1. Go to Google Cloud Console
  2. Create project → Enable Google Sheets API
  3. Create Service Account → Download JSON key
  4. Save as credentials.json (gitignore it!)
  5. Share your Google Sheet with service account email

3. Basic Usage

import gspread
from google.oauth2.service_account import Credentials

# Authenticate
SCOPES = [
    'https://www.googleapis.com/​auth/​spreadsheets',
    'https://www.googleapis.com/​auth/​drive'
]
creds = Credentials.from_service_account_file('credentials.json', scopes=SCOPES)
client = gspread.authorize(creds)

# Open sheet and write data
sheet = client.open("My Dashboard").sheet1
sheet.update('A1', [['Header1', 'Header2', 'Header3']])
sheet.append_row(['Value1', 'Value2', 'Value3'])

Core Patterns

Pattern 1: Type System Integration

Google Sheets API expects specific types. Always convert Python types properly.

from decimal import Decimal
from datetime import date, datetime

def prepare_row_for_sheets(row: dict) -> list:
    """Convert Python types to Sheets-compatible values."""
    values = []
    for key, value in row.items():
        if isinstance(value, Decimal):
            # Decimal → float (Sheets doesn't understand Decimal)
            values.append(float(value))
        elif isinstance(value, (date, datetime)):
            # Date → ISO string (Sheets can parse this)
            values.append(value.strftime('%Y-%m-%d'))
        elif isinstance(value, bool):
            # Bool → uppercase string (Sheets convention)
            values.append('TRUE' if value else 'FALSE')
        elif value is None:
            # None → empty string (Sheets blank cell)
            values.append('')
        else:
            values.append(value)
    return values

# Usage
row = {
    'date': date(2025, 1, 9),
    'amount': Decimal('19.79'),
    'active': True,
    'notes': None
}
sheet.append_row(prepare_row_for_sheets(row))
# Writes: ['2025-01-09', 19.79, 'TRUE', '']

Pattern 2: Defensive Authentication

Validate credentials exist and work before attempting operations.

from pathlib import Path
import gspread
from google.oauth2.service_account import Credentials

class SheetsClient:
    def __init__(self, credentials_path: str, sheet_name: str):
        # Defensive: Validate credentials file exists
        creds_file = Path(credentials_path)
        if not creds_file.exists():
            raise FileNotFoundError(
                f"Credentials file not found: {credentials_path}\n"
                f"See docs/​GOOGLE_SETUP.md for setup instructions."
            )

        self.credentials_path = credentials_path
        self.sheet_name = sheet_name
        self.client = None
        self.sheet = None

    def connect(self):
        """Authenticate and connect to sheet."""
        SCOPES = [
            'https://www.googleapis.com/​auth/​spreadsheets',
            'https://www.googleapis.com/​auth/​drive'
        ]

        creds = Credentials.from_service_account_file(
            self.credentials_path,
            scopes=SCOPES
        )
        self.client = gspread.authorize(creds)

        # Defensive: Validate sheet exists and is accessible
        try:
            spreadsheet = self.client.open(self.sheet_name)
            self.sheet = spreadsheet.sheet1
        except gspread.exceptions.SpreadsheetNotFound:
            raise ValueError(
                f"Sheet not found: {self.sheet_name}\n"
                f"Make sure the sheet is shared with the service account email."
            )

        return self

    def write_row(self, row: list):
        """Write a row, with connection validation."""
        if not self.sheet:
            raise RuntimeError("Not connected. Call connect() first.")
        self.sheet.append_row(row)

Pattern 3: Batch Operations

Minimize API calls by batching operations.

def write_multiple_rows(sheet, rows: list[list]):
    """Write multiple rows in single API call."""
    if not rows:
        return

    # Get next empty row
    existing = len(sheet.get_all_values())
    start_row = existing + 1

    # Batch update (single API call vs N calls)
    cell_range = f'A{start_row}'
    sheet.update(cell_range, rows)

# ✅ GOOD: 1 API call for 100 rows
write_multiple_rows(sheet, hundred_rows)

# ❌ BAD: 100 API calls
for row in hundred_rows:
    sheet.append_row(row)  # Each call is an API request!

Pattern 4: Cell Formatting

Apply formatting after writing data.

from gspread_formatting import (
    format_cell_range,
    CellFormat,
    NumberFormat,
    Color,
    TextFormat
)

def setup_sheet_formatting(sheet):
    """Apply standard formatting to sheet."""

    # Header row: Bold, gray background
    header_format = CellFormat(
        backgroundColor=Color(0.9, 0.9, 0.9),
        textFormat=TextFormat(bold=True),
        horizontalAlignment='CENTER'
    )
    format_cell_range(sheet, 'A1:Z1', header_format)

    # Currency columns (e.g., columns F and H)
    currency_format = CellFormat(
        numberFormat=NumberFormat(type='CURRENCY', pattern='$#,##0.00')
    )
    format_cell_range(sheet, 'F:F', currency_format)
    format_cell_range(sheet, 'H:H', currency_format)

    # Percentage columns (e.g., columns G and J)
    percent_format = CellFormat(
        numberFormat=NumberFormat(type='PERCENT', pattern='0.00%')
    )
    format_cell_range(sheet, 'G:G', percent_format)
    format_cell_range(sheet, 'J:J', percent_format)

Pattern 5: API Response Parsing

Parse external API responses with type validation before writing.

from typing import Dict, List, Any
from decimal import Decimal

class APIResponseParser:
    """Parse external API response for Sheets."""

    def __init__(self, response: List[Dict[str, Any]]):
        # Defensive: Validate response structure
        if not response or not isinstance(response, list):
            raise ValueError("Invalid API response format")

        self.raw_data = response

    def parse_row(self, item: Dict[str, Any]) -> Dict[str, Any]:
        """Parse single item with type conversion."""
        # Type conversion: API returns strings, we need proper types
        return {
            'date': item.get('date_start', ''),
            'name': item.get('name', ''),
            # String → int
            'impressions': int(item.get('impressions', '0')),
            'clicks': int(item.get('clicks', '0')),
            # String → Decimal (currency precision)
            'spend': Decimal(item.get('spend', '0')),
            # String → float
            'ctr': float(item.get('ctr', '0')),
        }

    def to_sheets_rows(self) -> List[List[Any]]:
        """Convert all items to Sheets row format."""
        rows = []
        for item in self.raw_data:
            parsed = self.parse_row(item)
            row = [
                parsed['date'],
                parsed['name'],
                parsed['impressions'],
                parsed['clicks'],
                float(parsed['spend']),  # Decimal → float for Sheets
                parsed['ctr'],
            ]
            rows.append(row)
        return rows

    @staticmethod
    def get_headers() -> List[str]:
        """Column headers matching to_sheets_rows() format."""
        return ['Date', 'Name', 'Impressions', 'Clicks', 'Spend', 'CTR']

Common Operations

Read Data

# Get all data
all_values = sheet.get_all_values()

# Get specific range
cell_range = sheet.get('A1:D10')

# Get as dictionaries (first row = headers)
records = sheet.get_all_records()
# Returns: [{'Name': 'Alice', 'Age': 30}, {'Name': 'Bob', 'Age': 25}]

# Find a cell
cell = sheet.find('search term')
print(f"Found at row {cell.row}, col {cell.col}")

Write Data

# Update single cell
sheet.update('A1', 'Hello')

# Update range
sheet.update('A1:C1', [['Col1', 'Col2', 'Col3']])

# Append row (to end of data)
sheet.append_row(['New', 'Row', 'Data'])

# Insert row at specific position
sheet.insert_row(['Inserted', 'Row'], index=2)

# Batch update multiple ranges
sheet.batch_update([
    {'range': 'A1', 'values': [['Header1']]},
    {'range': 'B1', 'values': [['Header2']]},
])

Formulas

# Write formula
sheet.update('D2', '=SUM(A2:C2)')

# Write multiple formulas
formulas = [
    ['=SUM(A2:A10)'],
    ['=AVERAGE(B2:B10)'],
    ['=MAX(C2:C10)'],
]
sheet.update('D2:D4', formulas)

# Summary row with formulas
summary_row = 12
sheet.update(f'A{summary_row}:F{summary_row}', [[
    'TOTAL',
    f'=SUM(B2:B{summary_row-1})',
    f'=SUM(C2:C{summary_row-1})',
    f'=SUM(D2:D{summary_row-1})',
    f'=AVERAGE(E2:E{summary_row-1})',
    f'=SUM(F2:F{summary_row-1})',
]])

Worksheets

# List all worksheets
worksheets = spreadsheet.worksheets()

# Get worksheet by name
ws = spreadsheet.worksheet("Sheet2")

# Create new worksheet
new_ws = spreadsheet.add_worksheet(title="New Sheet", rows=100, cols=20)

# Delete worksheet
spreadsheet.del_worksheet(ws)

# Duplicate worksheet
spreadsheet.duplicate_sheet(source_sheet_id=ws.id, new_sheet_name="Copy")

Error Handling

Common Errors and Solutions

ErrorCauseSolution
SpreadsheetNotFoundSheet not shared with service accountShare sheet with SA email
APIError: PERMISSION_DENIEDWrong permissionsGrant "Editor" access
APIError: RATE_LIMIT_EXCEEDEDToo many API callsAdd delays, use batch operations
FileNotFoundErrorcredentials.json missingDownload from GCP Console
RefreshErrorToken expiredRe-download credentials

Retry Pattern

import time
from gspread.exceptions import APIError

def write_with_retry(sheet, data, max_retries=3):
    """Write data with exponential backoff retry."""
    for attempt in range(max_retries):
        try:
            sheet.append_row(data)
            return True
        except APIError as e:
            if 'RATE_LIMIT' in str(e) and attempt < max_retries - 1:
                wait_time = 2 ** attempt  # 1, 2, 4 seconds
                print(f"Rate limited. Waiting {wait_time}s...")
                time.sleep(wait_time)
            else:
                raise
    return False

Scheduling Options

Option 1: Local Cron (Free)

# crontab -e
0 8 * * * cd /​path/​to/​project && python update_sheets.py >> /​var/​log/​sheets.log 2>&1

Option 2: GitHub Actions (Free Tier)

# .github/​workflows/​daily-sync.yml
name: Daily Sheets Sync

on:
  schedule:
    - cron: '0 8 * * *'  # Daily at 8 AM UTC
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/​checkout@v4
      - uses: actions/​setup-python@v5
        with:
          python-version: '3.11'
      - run: pip install -r requirements.txt
      - run: python update_sheets.py
        env:
          GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}

Option 3: AWS Lambda

# lambda_handler.py
import json
import base64
import os
from google.oauth2.service_account import Credentials

def handler(event, context):
    # Decode credentials from environment variable
    creds_json = base64.b64decode(os.environ['GOOGLE_CREDENTIALS_B64'])
    creds_dict = json.loads(creds_json)

    creds = Credentials.from_service_account_info(creds_dict)
    client = gspread.authorize(creds)

    # Your logic here
    sheet = client.open("Dashboard").sheet1
    sheet.append_row(['Updated', 'from', 'Lambda'])

    return {'statusCode': 200}

Security Checklist

  • credentials.json in .gitignore
  • File permissions: chmod 600 credentials.json
  • Service Account has minimal permissions (Sheets API only)
  • Sheet shared with specific SA email (not "anyone with link")
  • Rotate keys every 90 days
  • Don't log credentials or access tokens

File Organization

.claude/​skills/​google-sheets/
├── SKILL.md              # This file (entry point)
├── SETUP.md              # Service Account setup guide
├── PATTERNS.md           # Advanced patterns
└── examples/
    ├── basic_write.py    # Simple read/​write example
    └── api_to_sheets.py  # API → Sheets pipeline

Integration with Other Skills

ScenarioUse With
Need .env for credentials pathpython-env
Deploying to Lambdadeployment
Testing Sheets integrationtesting-workflow

References

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View