Install
openclaw skills install gspread-sheetsBatch read/write Google Sheets using the gspread Python library with service account authentication. Use when the user needs to: (1) read/write/update/clear...
openclaw skills install gspread-sheetspip install gspread google-auth
Service account JSON key file required. Set path via:
export GOOGLE_SERVICE_ACCOUNT_JSON=/path/to/key.jsonShare the target spreadsheet with the service account email (found in the JSON key file).
import gspread
from google.oauth2.service_account import Credentials
SCOPES = ['https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive']
# Option 1: From JSON file path
creds = Credentials.from_service_account_file('key.json', scopes=SCOPES)
gc = gspread.authorize(creds)
# Option 2: From JSON string (e.g. env var)
import json, os
creds_info = json.loads(os.environ['GOOGLE_SERVICE_ACCOUNT_JSON'])
creds = Credentials.from_service_account_info(creds_info, scopes=SCOPES)
gc = gspread.authorize(creds)
# By name
sh = gc.open("My Spreadsheet")
# By URL
sh = gc.open_by_url("https://docs.google.com/spreadsheets/d/SPREADSHEET_ID/edit")
# By key
sh = gc.open_by_key("SPREADSHEET_ID")
ws = sh.sheet1 # or sh.worksheet("Sheet Name")
# All values as list of lists
data = ws.get_all_values()
# All records (first row = headers)
records = ws.get_all_records()
# Specific range
subset = ws.get("A1:D10")
# Single cell
val = ws.acell("B2").value
val = ws.cell(row=2, col=2).value
# Column or row as list
col_a = ws.col_values(1)
row_1 = ws.row_values(1)
# Update a range (list of lists)
ws.update("A1:D3", [["a1","b1","c1","d1"],
["a2","b2","c2","d2"],
["a3","b3","c3","d3"]])
# Append rows (at the bottom)
ws.append_rows([["new1","row1"],["new2","row2"]])
# Append single row
ws.append_row(["val1","val2","val3"])
# Batch update multiple ranges at once (most efficient)
ws.batch_update([
{"range": "A1:B2", "values": [["a","b"],["c","d"]]},
{"range": "D1:E2", "values": [["x","y"],["z","w"]]},
])
# Update specific cells by row/col
cell_list = ws.range("A1:C2")
for i, cell in enumerate(cell_list):
cell.value = f"data_{i}"
ws.update_cells(cell_list)
# Clear specific range
ws.clear()
# Clear range
ws.batch_clear(["A1:D10", "F1:G5"])
# Format cells
ws.format("A1:D1", {"textFormat": {"bold": True}})
# List all worksheets
worksheets = sh.worksheets()
# Add worksheet
new_ws = sh.add_worksheet(title="New Sheet", rows=100, cols=20)
# Delete worksheet
sh.del_worksheet(ws)
# Rename
ws.update_title("New Name")
# Duplicate
ws.duplicate(new_sheet_name="Copy")
# Find all matches
cells = ws.findall("search_term")
# Find with regex
import re
cells = ws.findall(re.compile(r"^prefix_.*"))
# Replace all
ws.replace("old_value", "new_value")
data = ws.get_all_records() # list of dicts
for row in data:
row["status"] = "processed"
# Write back (need headers + data as list of lists)
header = list(data[0].keys())
rows = [list(r.values()) for r in data]
ws.update("A1", [header] + rows)
import math
BATCH_SIZE = 1000
for i in range(0, len(all_rows), BATCH_SIZE):
batch = all_rows[i:i+BATCH_SIZE]
ws.append_rows(batch, value_input_option="USER_ENTERED")
for ws_name in ["Sheet1", "Sheet2", "Sheet3"]:
ws = sh.worksheet(ws_name)
records = ws.get_all_records()
# process...
ws.batch_update(processed_ranges)
ws.add_conditional_formatting("A1:A100", {
"type": "NUMBER_GREATER",
"value": 100,
"format": {"backgroundColor": {"red": 0.8, "green": 0.2, "blue": 0.2}}
})
import gspread
from gspread.exceptions import APIError, WorksheetNotFound
try:
ws = sh.worksheet("NonExistent")
except WorksheetNotFound:
ws = sh.add_worksheet("NonExistent", rows=100, cols=20)
# Rate limit handling
import time
from gspread.exceptions import APIError
def safe_batch_update(ws, data, retries=3):
for attempt in range(retries):
try:
return ws.batch_update(data)
except APIError as e:
if "429" in str(e) and attempt < retries - 1:
time.sleep(2 ** attempt)
else:
raise
batch_update over multiple update calls — fewer API requestsvalue_input_option="USER_ENTERED" parses formulas and number formatsvalue_input_option="RAW" writes literal stringsinclude_empty=False in get_all_values omits trailing empty cellsgc.open_by_key() (more reliable than name)